/*
 * Copyright (C) 2024-present Puter Technologies Inc.
 * 
 * This file is part of Puter.
 * 
 * Puter is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

const configurable_auth = require("../../middleware/configurable_auth");
const { Endpoint } = require("../../util/expressutil");
const BaseService = require("../BaseService");
const fs = require("node:fs");

const { createWorker, setCloudflareKeys, deleteWorker } = require("./workerUtils/cloudflareDeploy");
const { getUserInfo } = require("./workerUtils/puterUtils");
const { LLRead } = require("../../filesystem/ll_operations/ll_read");
const { Context } = require("../../util/context");
const { NodePathSelector, NodeUIDSelector } = require("../../filesystem/node/selectors");
const { calculateWorkerNameNew } = require("./workerUtils/nameUtils");
const { Entity } = require("../../om/entitystorage/Entity");
const { SKIP_ES_VALIDATION } = require("../../om/entitystorage/consts");
const { Eq, StartsWith } = require("../../om/query/query");
const { get_app, subdomain } = require("../../helpers");
const { UsernameNotifSelector } = require("../NotificationService");
const APIError = require("../../api/APIError");
const FSNodeParam = require("../../api/filesystem/FSNodeParam");
const { UserActorType } = require("../auth/Actor");

async function readPuterFile(actor, filePath) {
    try {
        const svc_fs = this.services.get('filesystem');
        const node = await svc_fs.node(new NodePathSelector(filePath));
        const ll_read = new LLRead();
        const stream = await ll_read.run({
            fsNode: node,
            actor,
        });
        const chunks = [];
        let bytes = 0;
        stream.on("data", (data) => {
            chunks.push(data)
            bytes += data.byteLength
            if (bytes > 10 ** 7) {
                const err = Error("Worker source code must not exceed 10MB");
                stream.emit("error", err);
                throw err;
            }
        });
        return new Promise((res, rej) => {
            stream.on("error", (e) => {
                rej(e.toString());
            });
            stream.on("end", () => {
                res(Buffer.concat(chunks));
            })
        })
    } catch (e) {
        console.error(e)
    }


}
// This file is generated by webpack. To rebuild: cd to this directory and run `npm run build`
let preamble;
try {
    preamble = fs.readFileSync(__dirname + "/dist/workerPreamble.js", "utf-8");
} catch (e) {
    preamble = "";
    console.error("WORKERS ERROR: Preamble has not been built! Workers will not have access to puter.js\nTo fix this cd into src/backend/src/worker and run npm run build")
}
const PREAMBLE_LENGTH = preamble.split("\n").length - 1
class WorkerService extends BaseService {
    _init() {
        setCloudflareKeys(this.config);

        // Services used
        const svc_event = this.services.get('event');
        const svc_su = this.services.get("su");
        const es_subdomain = this.services.get('es:subdomain');
        const svc_auth = this.services.get("auth");
        const svc_notification = this.services.get('notification');

        svc_event.on('fs.write.file', async (_key, data, meta) => {
            // Code should only run on the same server as the write
            if (meta.from_outside) return;

            // Check if the file that was written correlates to a worker
            const results = await svc_su.sudo(async () => {
                return await es_subdomain.select({ predicate: new Eq({ key: "root_dir", value: data.node }) });
            });
            if (!results || results.length === 0)
                return;

            for (const result of results) {
                // Person who just wrote file (not necessarily file owner)
                const actor = Context.get("actor");

                // Worker data
                const fileData = (await readPuterFile(Context.get("actor"), data.node.path)).toString();
                const workerName = (await result.get("subdomain")).split(".").pop();

                // Get appropriate deploy time auth token to give to the worker 
                let authToken;
                const appOwner = await result.get("app_owner");
                if (appOwner) { // If the deployer is an app...
                    const appID = await appOwner.get("uid");
                    authToken = await svc_su.sudo(await data.node.get("owner"), async () => {
                        return await svc_auth.get_user_app_token(appID);
                    })
                } else { // If the deployer is not attached to any application
                    authToken = (await svc_auth.create_session_token((await data.node.get("owner")).type.user)).token
                }


                // svc_notification.notify(
                //     UsernameNotifSelector(actor.type.user.username),
                //     {
                //         source: 'worker',
                //         title: `Deploying CF worker ${workerName}`,
                //         template: 'user-requesting-share',
                //         fields: {
                //             username: actor.type.user.username,
                //         },
                //     }
                // );
                try {
                    // Create the worker
                    const cfData = await createWorker((await data.node.get("owner")).type.user, authToken, workerName, preamble + fileData, PREAMBLE_LENGTH);

                    // Send user the appropriate notification
                    if (cfData.success) {
                        // svc_notification.notify(
                        //     UsernameNotifSelector(actor.type.user.username),
                        //     {
                        //         source: 'worker',
                        //         title: `Succesfully deployed ${cfData.url}`,
                        //         template: 'user-requesting-share',
                        //         fields: {
                        //             username: actor.type.user.username,
                        //         },
                        //     }
                        // );
                    } else {
                        svc_notification.notify(
                            UsernameNotifSelector(actor.type.user.username),
                            {
                                source: 'worker',
                                title: `Failed to deploy ${workerName}! ${cfData.errors}`,
                                template: 'user-requesting-share',
                                fields: {
                                    username: actor.type.user.username,
                                },
                            }
                        );
                    }


                } catch (e) {
                    svc_notification.notify(
                        UsernameNotifSelector(actor.type.user.username),
                        {
                            source: 'worker',
                            title: `Failed to deploy ${workerName}!!\n ${e}`,
                            template: 'user-requesting-share',
                            fields: {
                                username: actor.type.user.username,
                            },
                        }
                    );
                }
            }
        });
    }
    static IMPLEMENTS = {
        ['workers']: {
            /**
             * 
             * @param {{filePath: string, workerName: string, authorization: string}} param0 
             * @returns {any}
             */
            async create({ filePath, workerName, authorization, appId }) {
                try {
                    workerName = workerName.toLocaleLowerCase(); // just incase
                    const svc_su = this.services.get("su");
                    const es_subdomain = this.services.get('es:subdomain');
                    const svc_auth = this.services.get("auth");

                    const currentDomains = await svc_su.sudo(Context.get("actor").get_related_actor(UserActorType), async () => {
                        return (await es_subdomain.select({ predicate: new StartsWith({ key: "subdomain", value: "workers.puter." }) }));
                    });

                    if (appId) {
                        const app = await get_app({uid: appId});
                        if (Context.get("actor").type.user.id !== app.owner_user_id)
                            throw APIError.create('no_suitable_app', null, { entry_name: workerName });

                        authorization = await svc_auth.get_user_app_token(appId);
                    }

                    if (currentDomains.length >= 100) {
                        throw APIError.create('subdomain_limit_reached', null, {isWorker: true, limit: 100});
                    }

                    if (this.global_config.reserved_words.includes(workerName)) {
                        throw APIError.create('subdomain_reserved', null, {
                            subdomain: workerName,
                        });
                    }

                    if (!(/^[a-zA-Z0-9_-]+$/.test(workerName))) return;

                    filePath = await (await (new FSNodeParam('path')).consolidate({
                        req: { user: Context.get("actor").type.user },
                        getParam: () => filePath,
                    })).get("path");

                    const userData = await getUserInfo(authorization, this.global_config.api_base_url);
                    const actor = Context.get("actor");
                    if (appId) {
                        await svc_su.sudo(await svc_auth.authenticate_from_token(authorization), async()=> {
                            await Context.sub({ [SKIP_ES_VALIDATION]: true }).arun(async () => {
                                const entity = await Entity.create({ om: es_subdomain.om }, {
                                    subdomain: "workers.puter." + calculateWorkerNameNew(userData, workerName),
                                    root_dir: filePath
                                });
                                await es_subdomain.upsert(entity);
                            });
                        });
                    } else {
                        await Context.sub({ [SKIP_ES_VALIDATION]: true }).arun(async () => {
                            const entity = await Entity.create({ om: es_subdomain.om }, {
                                subdomain: "workers.puter." + calculateWorkerNameNew(userData, workerName),
                                root_dir: filePath
                            });
                            await es_subdomain.upsert(entity);
                        });
                    }


                    const fileData = (await readPuterFile(actor, filePath)).toString();
                    const cfData = await createWorker(userData, authorization, calculateWorkerNameNew(userData.uuid, workerName), preamble + fileData, PREAMBLE_LENGTH);


                    return cfData;
                } catch (e) {
                    if (e instanceof APIError)
                        throw e;
                    console.error(e)
                    return { success: false, errors: e }
                }
            },
            async destroy({ workerName, authorization }) {
                try {
                    workerName = workerName.toLocaleLowerCase(); // just incase
                    const svc_su = this.services.get("su");
                    const es_subdomain = this.services.get('es:subdomain');

                    const userData = await getUserInfo(authorization, this.global_config.api_base_url);

                    const [result] = (await es_subdomain.select({ predicate: new Eq({ key: "subdomain", value: "workers.puter." + calculateWorkerNameNew(undefined, workerName) }) }));

                    if (result.values_.owner.uuid !== userData.uuid) {
                        throw new Error("This is not your worker!");
                    }

                    const cfData = await deleteWorker(userData, authorization, workerName);


                    await es_subdomain.delete(await result.get("uid"));
                    return cfData;


                } catch (e) {
                    if (e instanceof APIError)
                        throw e;
                    console.error(e);
                    return { success: false, e }
                }
            },
            async getFilePaths({workerName}) {
                try {
                    const es_subdomain = this.services.get('es:subdomain');
                    let currentDomains;
                    if (typeof(workerName) !== "string") {
                        currentDomains = (await es_subdomain.select({ predicate: new StartsWith({ key: "subdomain", value: "workers.puter." }) }));
                    } else {
                        currentDomains = (await es_subdomain.select({ predicate: new Eq({ key: "subdomain", value: "workers.puter." + workerName}) }));
                    }
                    const svc_fs = this.services.get('filesystem');

                    const domainToPath = []
                    for (const domain of currentDomains) {
                        const node = await domain.get("root_dir")
                        const subdomainString = (await domain.get("subdomain"))
                        let file_path = null;
                        let file_uid = null;
                        try {
                            file_path = await node.get("path");
                            file_uid = await node.get("uid");
                        } catch (e) {
                        }
                        domainToPath.push({ name: subdomainString.split(".").pop(), url: `https://${subdomainString}`, file_path, file_uid, created_at: (new Date(await domain.get("created_at"))).toISOString() });
                    }
                    return domainToPath;
                } catch (e) {
                    console.error(e)
                }

            },
            async startLogs({ workerName, authorization }) {
                return await this.exec_({ runtime, code });
            },
            async endLogs({ workerName, authorization }) {
                return await this.exec_({ runtime, code });
            },
        }
    }
    async ['__on_driver.register.interfaces']() {
        const svc_registry = this.services.get('registry');
        const col_interfaces = svc_registry.get('interfaces');

        col_interfaces.set('workers', {
            description: 'Execute code with various languages.',
            methods: {
                getFilePaths: {
                    description: 'get paths for your workers',
                    parameters: {
                        workerName: {
                            type: "string",
                            description: "Optionally, the name of the worker you want the path for"
                        }
                    },
                    result: {type: 'json'}
                },
                create: {
                    description: 'Create a backend worker',
                    parameters: {
                        filePath: {
                            type: "string",
                            description: "The path of the code of the worker to upload"
                        },
                        workerName: {
                            type: "string",
                            description: "The name of the worker you want to upload"
                        },
                        authorization: {
                            type: "string",
                            description: "Puter token"
                        },
                        appId: {
                            type: "string",
                            description: "App ID to tie a worker to"
                        }
                    },
                    result: { type: 'json' },
                },
                startLogs: {
                    description: 'Get logs for your backend worker',
                    parameters: {
                        workerName: {
                            type: "string",
                            description: "The name of the worker you want the logs of"
                        },
                        authorization: {
                            type: "string",
                            description: "Puter token"
                        }
                    },
                    result: { type: 'json' },
                },
                endLogs: {
                    description: 'Get logs for your backend worker',
                    parameters: {
                        workerName: {
                            type: "string",
                            description: "The name of the worker you want the logs of"
                        },
                        authorization: {
                            type: "string",
                            description: "Puter token"
                        }
                    },
                    result: { type: 'json' },
                },
                destroy: {
                    description: 'Get rid of your backend worker',
                    parameters: {
                        workerName: {
                            type: "string",
                            description: "The name of the worker you want to destroy"
                        },
                        authorization: {
                            type: "string",
                            description: "Puter token"
                        }
                    },
                    result: { type: 'json' },
                },
            }
        });
    }
}

module.exports = {
    WorkerService,
};
